/*
*    Qizx/open 3.0 beta
*
* This code is the open-source version of Qizx.
* Copyright (C) 2004-2009 Axyana Software -- All rights reserved.
*
* The contents of this file are subject to the Mozilla Public License 
*  Version 1.1 (the "License"); you may not use this file except in 
*  compliance with the License. You may obtain a copy of the License at
*  http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
*  WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
*  for the specific language governing rights and limitations under the
*  License.
*
* The Initial Developer of the Original Code is Xavier Franc - Axyana Software.
*
*/
using System;
using System.Qizx.Util;
using System.Text.RegularExpressions;
using Thesaurus = System.Qizx.Api.FullText.Thesaurus;
using LookupResult = System.Qizx.Api.FullText.LookupResult;
using TokenSequence = System.Qizx.Api.FullText.TokenSequence;
using Displayer = System.Qizx.Queries.Query.Displayer;
using Instantiator = System.Qizx.Queries.Query.Instantiator;
using System.Qizx.Queries.Iterators;
using RegexMatchPattern = System.Qizx.Util.RegexMatchPattern;
using Unicode = System.Qizx.Util.Basic.Unicode;

namespace System.Qizx.Queries {

    /// <summary> Full-Text Query wrapper, implementing standard XQuery Full-Text specifications.
    /// <p>
    /// 
    /// </summary>
    /// <summary> Super-class of all 'Full-text Selections' at the right hand-side of the
    /// ftcontains operator.
    /// </summary>
    public abstract class Selection {
        virtual public MatchOptions MatchOptions {
            get {
                return matchOptions;
            }

            set {
                this.matchOptions = value;
            }

        }
        virtual public float Weight {
            get {
                return weight;
            }

            set {
                this.weight = value;
            }

        }
        protected internal PosFilters posFilters; // optional
        protected internal MatchOptions matchOptions; // optional
        protected internal float weight = 1; // scoring
        // 'occurs times' option is allowed only on simple terms or phrases but 
        // for simplicity and generality we put it here:
        public int[] occRange;

        public virtual PosFilters getPosFilters() {
            return posFilters;
        }

        public virtual void setPosFilters(PosFilters posFilters) {
            this.posFilters = posFilters;
        }

        public abstract PostingIterator realize(Query.Instantiator factory);

        public abstract void display(Query.Displayer display);

        public virtual void displayFilters(Query.Displayer display) {
            if (posFilters != null) {
                if (posFilters.ordered)
                    display.print(" ordered");
                if (posFilters.distanceRange != null) {
                    display.print(" dist " + posFilters.distanceRange[0] + " " + posFilters.distanceRange[1]);
                }
                if (posFilters.window > 0) {
                    display.print(" window " + posFilters.window);
                }
            }
            if (occRange != null) {
                display.print(" occ[" + occRange[0] + " " + occRange[1] + "]");
            }
            if (weight != 1)
                display.print(" w=" + weight);
        }

        protected internal virtual PostingIterator filtered(PostingIterator iterator) {
            if (posFilters != null && iterator is PolyIterator) {
                PolyIterator polite = (PolyIterator)iterator;
                if (posFilters.ordered)
                    iterator.setOrdered(true);
                if (posFilters.windowUnit == PosFilters.WORDS) {
                    polite.WindowConstraint = posFilters.window;
                }
                if (posFilters.distanceRange != null) {
                    polite.setDistanceConstraint(posFilters.distanceRange[0], posFilters.distanceRange[1]);
                }
                if (posFilters.content != PosFilters.UNSPECIFIED) {
                    polite.ContentConstraint = posFilters.content;
                }
            }
            iterator.Weight = weight;
            if (occRange != null) {
                return new CountIterator(occRange[0], occRange[1], iterator);
            }
            return iterator;
        }

        public abstract Selection applyThesauri();

        protected internal virtual Selection expand(TokenSequence sequence, MatchOptions matchOptions, float weight) {
            if (sequence.size() == 1) {
                SimpleWord w = new SimpleWord(sequence.getTokenAt(0));
                return w.decorate(matchOptions, weight);
            }
            Phrase ph = new Phrase();
            for (int t = 0; t < sequence.size(); t++) {
                SimpleWord w = new SimpleWord(sequence.getTokenAt(t));
                ph.addChild(w.decorate(matchOptions, weight));
            }
            return ph.decorate(matchOptions, weight);
        }

        protected internal virtual Selection decorate(MatchOptions matchOptions, float weight) {
            this.weight = weight;
            this.matchOptions = matchOptions;
            return this;
        }

        protected internal virtual LookupResult matchThesauri(Thesaurus[] thesauri, TokenSequence tokens) {
            LookupResult best = null;
            for (int i = 0; i < thesauri.Length; i++) {
                LookupResult res = thesauri[i].lookup(tokens);
                if (res == null)
                    continue;
                int ct = res.consumedTokens();
                if (best == null || ct > best.consumedTokens())
                    best = res;
                else if (ct == best.consumedTokens()) {
                    for (int t = 0; t < ct; t++)
                        best.addSequence(res.getSequence(t));
                }
            }
            return best;
        }
    }
    // super class for Or And MildNot
    public abstract class SelectionList : Selection {
        virtual public int ChildCount {
            get {
                return (subs == null) ? 0 : subs.Length;
            }

        }
        protected internal Selection[] subs;

        public SelectionList(Selection s) {
            if (s != null)
                this.subs = new Selection[] { s };
        }

        public SelectionList(Selection s1, Selection s2) {
            this.subs = new Selection[] { s1, s2 };
        }

        public virtual Selection getChild(int index) {
            return index < 0 || index >= subs.Length ? null : subs[index];
        }

        public virtual void setChild(int index, Selection s) {
            subs[index] = s;
        }

        public virtual void setChildren(Selection s1, Selection s2) {
            this.subs = new Selection[] { s1, s2 };
        }

        public virtual void addChild(Selection s) {
            Selection[] old = subs;
            int olen = (old == null) ? 0 : old.Length;
            subs = new Selection[olen + 1];
            if (old != null)
                Array.Copy(old, 0, subs, 0, olen);
            subs[olen] = s;
        }

        protected internal virtual void displayList(Displayer display, string title) {
            display.print(title);
            display.print("(");
            if (subs != null)
                for (int i = 0; i < subs.Length; i++) {
                    if (i > 0)
                        display.print(", ");
                    subs[i].display(display);
                }
            display.print(")");
            displayFilters(display);
        }
    }
    /// <summary> Alternative between selections</summary>
    public class Or : SelectionList {
        public Or(Selection s1, Selection s2)
            : base(s1, s2) {
        }

        public Or()
            : base(null) {
        }

        public override PostingIterator realize(Query.Instantiator factory) {
            if (subs.Length == 1) {
                PostingIterator iter = subs[0].realize(factory);
                if (iter is PolyIterator)
                    return filtered((PolyIterator)iter);
                return iter;
            }
            PostingIterator[] its = new PostingIterator[subs.Length];
            for (int i = 0; i < subs.Length; ++i) {
                its[i] = subs[i].realize(factory);
            }
            return filtered(new OrIterator(its));
        }

        public override void display(Displayer display) {
            displayList(display, "Or");
        }

        public override Selection applyThesauri() {
            // inline inner Or's
            for (int i = 0, len = subs.Length; i < len; ++i) {
                subs[i] = subs[i].applyThesauri();
                if (subs[i] is Or && subs[i].matchOptions == matchOptions && subs[i].posFilters == null) {
                    Or tmp = (Or)subs[i];
                    subs[i] = tmp.getChild(0);
                    for (int c = 1; c < tmp.ChildCount; c++)
                        addChild(tmp.getChild(c));
                }
            }
            return this;
        }
    }
    /// <summary> Conjunction of selections</summary>
    public class And : SelectionList {
        public And(Selection s1, Selection s2)
            : base(s1, s2) {
        }

        public And()
            : base(null) {
        }

        public override PostingIterator realize(Query.Instantiator factory) {
            bool order = posFilters != null && posFilters.ordered;
            if (subs.Length == 1 && posFilters == null) {
                return subs[0].realize(factory);
            }
            PostingIterator[] its = new PostingIterator[subs.Length];
            for (int i = 0; i < subs.Length; ++i) {
                its[i] = subs[i].realize(factory);
                if (order)
                    its[i].setOrdered(true);
            }
            return filtered(new AllIterator(its));
        }

        public override void display(Displayer display) {
            displayList(display, "And");
        }

        public override Selection applyThesauri() {
            // simply replace each term
            for (int i = 0; i < subs.Length; ++i) {
                subs[i] = subs[i].applyThesauri();
            }
            return this;
        }
    }
    /// <summary> OPerator 'not in' or mild-not.</summary>
    public class MildNot : Selection {
        protected internal Selection what, notin;

        public virtual void setChildren(Selection what, Selection where) {
            this.what = what;
            this.notin = where;
        }

        public override PostingIterator realize(Query.Instantiator factory) {
            return new MildNotIterator(what.realize(factory), notin.realize(factory));
        }

        public override void display(Displayer display) {
            display.print("Notin(");
            what.display(display);
            display.print(", ");
            notin.display(display);
            display.print(")");
            displayFilters(display);
        }

        public override Selection applyThesauri() {
            what = what.applyThesauri();
            notin = notin.applyThesauri();
            return this;
        }
    }
    public class Not : Selection {
        virtual public Selection Child {
            get {
                return child;
            }

            set {
                this.child = value;
            }

        }
        protected internal Selection child;

        public override PostingIterator realize(Query.Instantiator factory) {
            // 'not' means exactly 0 occurrences
            return new CountIterator(0, 0, child.realize(factory));
        }

        public override void display(Displayer display) {
            display.print("Not(");
            child.display(display);
            display.print(")");
            displayFilters(display);
        }

        public override Selection applyThesauri() {
            child = child.applyThesauri();
            return this;
        }
    }
    public class Phrase : SelectionList {
        public Phrase()
            : base(null) {
        }

        public Phrase(Selection t1, Selection t2)
            : base(t1, t2) {
        }

        // overridden to add window if unspecified
        public override void setPosFilters(PosFilters posFilters) {
            this.posFilters = posFilters;
            if (posFilters != null && posFilters.window <= 0)
                posFilters.window = subs.Length;
        }

        public override PostingIterator realize(Query.Instantiator factory) {
            if (subs == null)
                return new OrIterator(new PostingIterator[0]); // always fails
            if (subs.Length == 1 && posFilters == null) {
                return subs[0].realize(factory);
            }
            bool variable = false;
            PostingIterator[] its = new PostingIterator[subs.Length];
            for (int i = subs.Length; --i >= 0; ) {
                its[i] = subs[i].realize(factory);
                // useful if sub iter is a Or (wildcard, stemming):
                its[i].setOrdered(true);
                if (its[i] is OrIterator)
                    variable = true;
            }
            return filtered(new PhraseIterator(its, variable));
        }

        public override void display(Displayer display) {
            displayList(display, "Phrase");
        }

        public override Selection applyThesauri() {
            Thesaurus[] thesauri = matchOptions.thesauri;
            if (thesauri == null)
                return this; // no need to descend
            // expand as a list of simple tokens
            TokenSequence ph = new TokenSequence();
            for (int i = 0; i < subs.Length; ++i) {
                if (subs[i] is SimpleWord) {
                    SimpleWord sw = (SimpleWord)subs[i];
                    ph.addToken(sw.word);
                }
                    // cant do
                else
                    return this;
            }
            // repeated multi-token lookup
            Phrase repl = new Phrase();
            for (int spos = 0; ph.size() > 0; ) {
                LookupResult match = matchThesauri(thesauri, ph);
                if (match == null) {
                    repl.addChild(subs[spos]);
                    ph.removeTokens(0, 1);
                    ++spos;
                } else {
                    spos += match.consumedTokens();
                    ph.removeTokens(0, match.consumedTokens());
                    Or or = new Or();
                    for (int i = match.size(); --i >= 0; ) {
                        or.addChild(expand(match.getSequence(i), matchOptions, weight));
                    }
                    repl.addChild(or);
                }
            }
            Selection result = repl;
            if (repl.ChildCount == 1)
                // simplify
                result = repl.getChild(0);
            result.posFilters = posFilters;
            return result.decorate(matchOptions, weight);
        }
    }
    /// <summary> Simple word selection.</summary>
    public class SimpleWord : Selection {
        internal char[] word;

        public SimpleWord(char[] token) {
            this.word = token;
        }

        public override PostingIterator realize(Query.Instantiator factory) {
            PostingIterator iter = factory.enumWord(word, matchOptions);
            return filtered(iter);
        }

        public override void display(Displayer display) {
            display.print("Term '" + new System.String(word) + "'");
            displayFilters(display);
        }

        public override Selection applyThesauri() {
            if (matchOptions != null && matchOptions.thesauri != null) {
                Thesaurus[] thes = matchOptions.thesauri;
                TokenSequence seq1 = new TokenSequence();
                seq1.addToken(word);
                LookupResult match = matchThesauri(thes, seq1);
                if (match != null) {
                    Or replacement = new Or();
                    for (int i = match.size(); --i >= 0; ) {
                        replacement.addChild(expand(match.getSequence(i), matchOptions, weight));
                    }
                    return replacement.decorate(matchOptions, weight);
                }
            }
            return this;
        }
    }
    public class Wildcard : Selection {
        internal char[] pattern;

        public Wildcard(char[] tokenChars) {
            pattern = tokenChars;
        }

        public override PostingIterator realize(Query.Instantiator factory) {
            return filtered(factory.enumWildcard(pattern, matchOptions));
        }

        public override void display(Displayer display) {
            display.print("Wildcard '" + new System.String(pattern) + "'");
            displayFilters(display);
        }

        public override Selection applyThesauri() {
            return this;
        }
    }
    /// <summary> Match options defined in static context or on primary FT Selections.</summary>
    public class MatchOptions {
        public const sbyte UNSPECIFIED = 0;
        public const sbyte SENSITIVE = 1;
        public const sbyte INSENSITIVE = 2;
        public const sbyte LOWERCASE = 3;
        public const sbyte UPPERCASE = 4;
        public const sbyte WITH = 1;
        public const sbyte WITHOUT = 2;

        public string language;
        public sbyte wildcards;
        public sbyte stemming;
        public sbyte diacritics;
        public sbyte caseSensitivity; // INSENSITIVE, SENSITIVE, LOWERCASE, UPPERCASE
        // list of resolved Thesaurus drivers:
        public Thesaurus[] thesauri;
        //        // contain URI or DefaultStopWordList's:
        //        public ArrayList addedStopWords;
        //        public ArrayList exceptedStopWords;

        public MatchOptions() {
            language = null;
            stemming = WITHOUT;
            wildcards = WITHOUT;
            diacritics = INSENSITIVE;
            caseSensitivity = INSENSITIVE;
        }

        public MatchOptions(MatchOptions options) {
            language = options.language;
            wildcards = options.wildcards;
            stemming = options.stemming;
            diacritics = options.diacritics;
            caseSensitivity = options.caseSensitivity;
            thesauri = options.thesauri;
            //            if(options.addedStopWords != null)
            //                addedStopWords = (ArrayList) options.addedStopWords.clone();  // deep copy?
            //            if(options.exceptedStopWords != null)
            //                exceptedStopWords = (ArrayList) options.exceptedStopWords.clone(); 
        }

        public virtual bool likeDefault() {
            return language == null && stemming == WITHOUT && wildcards == WITHOUT && diacritics == INSENSITIVE && caseSensitivity == INSENSITIVE && thesauri == null;
        }

        //        public void addStopWords(Object swords)
        //        {
        //            if(addedStopWords == null)
        //                addedStopWords = new ArrayList();
        //            addedStopWords.add(swords);
        //        }
        //        
        //        public void exceptStopWords(Object swords)
        //        {
        //            if(exceptedStopWords == null)
        //                exceptedStopWords = new ArrayList();
        //            exceptedStopWords.add(swords);
        //        }

        public virtual void addThesaurus(Thesaurus thesaurus) {
            if (thesauri == null)
                thesauri = new Thesaurus[] { thesaurus };
            else {
                Thesaurus[] old = thesauri;
                thesauri = new Thesaurus[old.Length + 1];
                Array.Copy(old, 0, thesauri, 0, old.Length);
                thesauri[old.Length] = thesaurus;
            }
        }

        public static int caseSense(MatchOptions matchOptions) {
            sbyte cs = (matchOptions != null) ? matchOptions.caseSensitivity : INSENSITIVE;
            return cs == UNSPECIFIED ? INSENSITIVE : cs;
        }

        public static bool diacSense(MatchOptions matchOptions) {
            return (matchOptions != null) ? (matchOptions.diacritics == SENSITIVE) : false;
        }

        public static RegexMatchPattern compilePattern(char[] pattern, int caseSense, bool diacriticsSense) {
            pattern = mapPattern(pattern, caseSense, diacriticsSense);
            int flags = (caseSense != INSENSITIVE) ? 0 : (Pattern.CASE_INSENSITIVE + Pattern.UNICODE_CASE);
            RegexMatchPattern pat = new RegexMatchPattern(Pattern.compile(new System.String(pattern), flags));
            pat.DiacriticsSensitive = diacriticsSense;
            return pat;
        }

        public static char[] mapPattern(char[] token, int caseSense, bool diacriticsSense) {
            char[] clone = null;
            if (caseSense == LOWERCASE || caseSense == UPPERCASE) {
                clone = new char[token.Length];
                token.CopyTo(clone, 0);
                if (caseSense == LOWERCASE)
                    Unicode.toLowerCase(clone);
                else
                    Unicode.toUpperCase(clone);
                token = clone;
            }
            if (!diacriticsSense) {
                if (clone == null) {
                    clone = new char[token.Length];
                    token.CopyTo(clone, 0);
                }
                token = clone;
                Unicode.collapseDiacritic(token);
            }
            return token;
        }
    }
    /// <summary> Position filters optionally defined on a FT Selection.
    /// <p>
    /// Position filtering options are: <ul>
    /// <li>Ordered or non-ordered (applicable only to a ftand)
    /// <li>Window and window-unit: the maximum span of a match expressed in
    /// words, sentences or paragraphs.
    /// <li>Distance and unit (applicable only to a ftand): the distance
    /// between matches (at least, at most, from..to).
    /// <li>Scope: same sentence, same paragraph, different sentence, different
    /// paragraph (<b>Not supported</b>).
    /// <li>Content: at start, at end, entire content
    /// </ul>
    /// </summary>
    public class PosFilters {
        public const int WORDS = 1;
        public const int SENTENCES = 2;
        public const int PARAGRAPHS = 3;

        public const int UNSPECIFIED = 0;
        public const int SAME_SENTENCE = 1;
        public const int SAME_PARAGRAPH = 2;
        public const int DIFF_SENTENCE = 3;
        public const int DIFF_PARAGRAPH = 4;

        public const int AT_START = 1;
        public const int AT_END = 2;
        public const int ENTIRE_CONTENT = 3;

        public bool ordered;
        public int window;
        public int windowUnit; // WORDS, SENTENCES, PARAGRAPHS
        public int[] distanceRange;
        public int distanceUnit; // WORDS, SENTENCES, PARAGRAPHS
        public int scope; // SAME_SENTENCE, DIFF_SENTENCE, SAME_PARAG, DIFF_PARAG
        public int content; // UNDEF, AT_START, AT_END, ENTIRE_CONTENT   

        public PosFilters() {
            distanceUnit = windowUnit = WORDS;
        }

        public PosFilters(bool ordered) {
            distanceUnit = windowUnit = WORDS;
            this.ordered = ordered;
        }

        public PosFilters(PosFilters filters) {
            ordered = filters.ordered;
            window = filters.window;
            windowUnit = filters.windowUnit;
            distanceRange = filters.distanceRange;
            distanceUnit = filters.distanceUnit;
            scope = filters.scope;
            content = filters.content;
        }
    }

    public struct FullText_Fields {
        public const int ANY = 0;
        public const int ANY_WORD = 1;
        public const int ALL = 2;
        public const int ALL_WORDS = 3;
        public const int PHRASE = 4;
    }
    public interface FullText {
    }
}